Learning goals¶

After today's lesson you should be understand:

  • Network modeling and analysis with NetworkX
  • Spatial network modeling and analysis with OSMnx and OpenStreetMap

Today's exercise was created by Geoff Boeing for his Advanced Urban Analytics class. Thanks Geoff!

In [1]:
import networkx as nx
import numpy as np
import osmnx as ox
import pandas as pd

# configure OSMnx
ox.settings.log_console = True
In [3]:
# anytime you're curious what package version you have, use __version__
print(nx.__version__)
print(ox.__version__)
2.8.8
1.2.2

1. Network analysis with NetworkX¶

Networks let you represent structure and interaction among the components of a system. In analytics, they let you go beyond models that average across individuals/components or treat the population/system as a monolith. Networks are useful when the system's structure is nontrivial.

A network is a set of objects (called nodes or vertices) connected to each other by a set of connections (called edges or links). A graph is a mathematical model of a network: usually used synonymously. You can represent a graph as an adjacency matrix to use the tools of linear algebra to analyze it. You can also simulate dynamics and flows on it.

A trivial (simple) network is undirected, unweighted, and lacks self-loops or parallel edges. A nontrivial (complex) network may be directed and weighted and have self-loops and parallel edges. A spatial network is a network that is spatially embedded. That means its nodes and/or edges have locations in space. A spatial network is defined by both its geometry (positions, distances, angles, etc) and its topology (connections and configurations).

Examples:

  • street networks
  • airline routes
  • rail lines
  • capital flows
  • spread of contagious diseases

We can analyze a network in various ways. To take street networks as an example, you can measure its compactness via intersection density, its connectedness via average node degree, or the relative importance of different nodes via centrality. Betweenness centrality measures what share of all shortest paths in a network pass through each node. Closeness centrality measures the average distance between a node and all other nodes in the network.

In [4]:
# create a random small-world graph of a social network
G = nx.watts_strogatz_graph(n=100, k=5, p=0.1, seed=0)

## Here we are creating a graph with 100 nodes, each node is connected to 5 other nodes, and the probability of rewiring is 0.1
## Rewiring means that the edge between two nodes is randomly reconnected to another node
In [5]:
nx.draw(G)
In [6]:
# how many nodes and edges?
print(len(G.nodes))
print(len(G.edges))
100
200
In [7]:
# assign random ages to each person in the network
randoms = np.random.randint(low=18, high=90, size=len(G.nodes))
ages = {node:age for node, age in zip(G.nodes, randoms)}
In [8]:
ages
Out[8]:
{0: 19,
 1: 28,
 2: 67,
 3: 30,
 4: 56,
 5: 68,
 6: 60,
 7: 24,
 8: 68,
 9: 20,
 10: 68,
 11: 52,
 12: 32,
 13: 39,
 14: 26,
 15: 39,
 16: 26,
 17: 63,
 18: 18,
 19: 52,
 20: 54,
 21: 61,
 22: 53,
 23: 48,
 24: 21,
 25: 55,
 26: 39,
 27: 73,
 28: 52,
 29: 52,
 30: 88,
 31: 80,
 32: 77,
 33: 58,
 34: 57,
 35: 78,
 36: 71,
 37: 33,
 38: 39,
 39: 39,
 40: 50,
 41: 35,
 42: 70,
 43: 48,
 44: 29,
 45: 83,
 46: 70,
 47: 64,
 48: 69,
 49: 68,
 50: 54,
 51: 33,
 52: 57,
 53: 55,
 54: 81,
 55: 54,
 56: 47,
 57: 44,
 58: 39,
 59: 23,
 60: 38,
 61: 56,
 62: 84,
 63: 65,
 64: 88,
 65: 82,
 66: 89,
 67: 82,
 68: 32,
 69: 73,
 70: 28,
 71: 32,
 72: 50,
 73: 20,
 74: 61,
 75: 72,
 76: 75,
 77: 56,
 78: 70,
 79: 48,
 80: 29,
 81: 68,
 82: 64,
 83: 62,
 84: 34,
 85: 54,
 86: 72,
 87: 19,
 88: 68,
 89: 40,
 90: 57,
 91: 40,
 92: 81,
 93: 83,
 94: 50,
 95: 47,
 96: 23,
 97: 80,
 98: 81,
 99: 49}
In [9]:
nx.set_node_attributes(G, values=ages, name='age')

`

In [10]:
# assign random "social distance" to each edge in the network
# social distance is the inverse of how often they hang out each year
hangout_counts = np.random.randint(low=1, high=100, size=len(G.edges))
distances = {edge: 1 / hangout_count for edge, hangout_count in zip(G.edges, hangout_counts)}
In [11]:
distances
Out[11]:
{(0, 1): 0.010309278350515464,
 (0, 99): 0.02857142857142857,
 (0, 2): 0.02,
 (0, 98): 0.017543859649122806,
 (1, 2): 0.2,
 (1, 3): 0.011235955056179775,
 (1, 99): 0.018867924528301886,
 (2, 3): 0.06666666666666667,
 (2, 4): 0.016666666666666666,
 (2, 17): 0.016129032258064516,
 (3, 4): 1.0,
 (3, 5): 0.011494252873563218,
 (4, 5): 0.018867924528301886,
 (4, 6): 0.014492753623188406,
 (4, 92): 0.041666666666666664,
 (5, 6): 0.04,
 (5, 7): 0.14285714285714285,
 (6, 7): 0.015151515151515152,
 (6, 73): 0.014285714285714285,
 (7, 8): 0.017543859649122806,
 (7, 9): 0.03333333333333333,
 (8, 9): 0.013333333333333334,
 (8, 10): 0.010101010101010102,
 (9, 10): 0.011235955056179775,
 (9, 11): 0.012658227848101266,
 (9, 27): 0.018518518518518517,
 (10, 11): 0.041666666666666664,
 (10, 15): 0.012345679012345678,
 (11, 12): 0.018518518518518517,
 (11, 13): 0.04,
 (12, 13): 0.010638297872340425,
 (12, 14): 0.014492753623188406,
 (12, 32): 0.5,
 (13, 14): 0.16666666666666666,
 (13, 15): 0.034482758620689655,
 (14, 15): 0.01694915254237288,
 (14, 16): 0.16666666666666666,
 (15, 16): 0.010526315789473684,
 (15, 17): 0.017857142857142856,
 (16, 17): 0.019230769230769232,
 (16, 18): 0.025,
 (17, 18): 0.016129032258064516,
 (18, 19): 0.017857142857142856,
 (18, 20): 0.015873015873015872,
 (19, 20): 0.01020408163265306,
 (19, 21): 0.022222222222222223,
 (20, 21): 0.014705882352941176,
 (20, 22): 0.1,
 (20, 87): 0.013333333333333334,
 (21, 22): 0.045454545454545456,
 (21, 23): 0.03225806451612903,
 (22, 23): 0.08333333333333333,
 (22, 24): 0.05263157894736842,
 (23, 24): 0.02,
 (23, 86): 0.047619047619047616,
 (24, 25): 0.041666666666666664,
 (24, 54): 0.01282051282051282,
 (24, 74): 0.011904761904761904,
 (25, 26): 0.018518518518518517,
 (25, 27): 0.015151515151515152,
 (26, 27): 0.010309278350515464,
 (26, 28): 0.02127659574468085,
 (27, 28): 0.3333333333333333,
 (28, 29): 0.037037037037037035,
 (28, 30): 0.015384615384615385,
 (29, 30): 0.029411764705882353,
 (29, 31): 0.018518518518518517,
 (30, 31): 0.017543859649122806,
 (30, 32): 0.011764705882352941,
 (31, 32): 0.022727272727272728,
 (31, 33): 0.029411764705882353,
 (32, 33): 0.02702702702702703,
 (33, 34): 0.011235955056179775,
 (33, 35): 0.010416666666666666,
 (34, 35): 0.011904761904761904,
 (34, 36): 0.022727272727272728,
 (34, 97): 0.02040816326530612,
 (35, 37): 0.023255813953488372,
 (35, 92): 0.014925373134328358,
 (35, 61): 0.024390243902439025,
 (36, 37): 0.04,
 (36, 38): 0.014285714285714285,
 (37, 38): 0.027777777777777776,
 (37, 39): 0.02564102564102564,
 (38, 39): 0.015384615384615385,
 (38, 40): 0.010416666666666666,
 (39, 40): 0.011235955056179775,
 (39, 41): 0.02040816326530612,
 (40, 41): 0.02702702702702703,
 (40, 42): 0.013513513513513514,
 (40, 50): 0.011111111111111112,
 (41, 42): 0.0136986301369863,
 (41, 43): 0.025,
 (42, 43): 0.011764705882352941,
 (42, 44): 0.010101010101010102,
 (43, 44): 0.011111111111111112,
 (43, 45): 0.010869565217391304,
 (44, 45): 0.017241379310344827,
 (44, 46): 0.02631578947368421,
 (45, 47): 0.05263157894736842,
 (45, 72): 0.027777777777777776,
 (46, 47): 0.03333333333333333,
 (46, 48): 0.041666666666666664,
 (47, 48): 0.03225806451612903,
 (47, 49): 0.022727272727272728,
 (48, 49): 0.01098901098901099,
 (48, 50): 0.125,
 (49, 50): 0.16666666666666666,
 (49, 51): 0.16666666666666666,
 (49, 63): 0.01818181818181818,
 (50, 87): 0.010526315789473684,
 (51, 52): 0.030303030303030304,
 (51, 53): 0.03571428571428571,
 (52, 53): 0.03225806451612903,
 (52, 54): 0.012048192771084338,
 (53, 54): 0.014084507042253521,
 (53, 55): 0.010526315789473684,
 (54, 55): 0.02702702702702703,
 (54, 56): 0.012195121951219513,
 (55, 56): 0.023809523809523808,
 (55, 57): 0.012987012987012988,
 (56, 57): 0.2,
 (56, 58): 0.045454545454545456,
 (57, 58): 0.012345679012345678,
 (57, 59): 0.011627906976744186,
 (58, 59): 0.037037037037037035,
 (58, 60): 1.0,
 (59, 60): 0.012048192771084338,
 (59, 94): 0.047619047619047616,
 (60, 61): 0.02040816326530612,
 (60, 94): 0.027777777777777776,
 (61, 62): 0.015873015873015872,
 (62, 63): 0.014492753623188406,
 (62, 64): 0.011627906976744186,
 (63, 65): 0.014285714285714285,
 (64, 65): 0.5,
 (64, 66): 0.012987012987012988,
 (65, 66): 0.03225806451612903,
 (65, 67): 0.06666666666666667,
 (66, 67): 0.03571428571428571,
 (66, 68): 0.019230769230769232,
 (67, 68): 0.0196078431372549,
 (67, 69): 0.14285714285714285,
 (68, 70): 0.045454545454545456,
 (68, 84): 0.05,
 (69, 70): 0.010101010101010102,
 (69, 71): 0.011764705882352941,
 (70, 72): 0.05263157894736842,
 (70, 86): 0.02702702702702703,
 (71, 72): 0.017241379310344827,
 (71, 73): 0.019230769230769232,
 (72, 73): 0.2,
 (72, 74): 0.023255813953488372,
 (73, 74): 1.0,
 (73, 75): 0.016666666666666666,
 (74, 75): 0.029411764705882353,
 (75, 76): 0.011235955056179775,
 (75, 77): 0.011235955056179775,
 (76, 77): 0.023255813953488372,
 (76, 78): 0.014285714285714285,
 (77, 78): 0.0136986301369863,
 (77, 79): 0.011494252873563218,
 (78, 79): 0.047619047619047616,
 (78, 80): 0.027777777777777776,
 (78, 92): 0.01020408163265306,
 (79, 80): 0.012195121951219513,
 (79, 81): 0.013513513513513514,
 (80, 81): 0.014925373134328358,
 (80, 82): 0.02702702702702703,
 (81, 82): 0.01282051282051282,
 (81, 83): 0.011494252873563218,
 (82, 83): 0.011111111111111112,
 (82, 89): 0.25,
 (83, 84): 0.010101010101010102,
 (83, 85): 0.05,
 (84, 85): 0.011494252873563218,
 (84, 86): 0.021739130434782608,
 (85, 86): 0.5,
 (85, 87): 0.02040816326530612,
 (86, 87): 0.01282051282051282,
 (86, 88): 0.037037037037037035,
 (87, 88): 0.5,
 (88, 89): 0.014492753623188406,
 (88, 90): 0.01020408163265306,
 (89, 90): 0.034482758620689655,
 (89, 91): 0.058823529411764705,
 (90, 91): 0.025,
 (90, 92): 0.017241379310344827,
 (91, 92): 0.030303030303030304,
 (91, 93): 0.014925373134328358,
 (93, 94): 0.010869565217391304,
 (93, 95): 0.020833333333333332,
 (94, 95): 0.023809523809523808,
 (94, 96): 0.020833333333333332,
 (95, 96): 0.010526315789473684,
 (95, 97): 0.02857142857142857,
 (96, 97): 0.024390243902439025,
 (96, 98): 0.015625,
 (97, 99): 0.015873015873015872,
 (98, 99): 0.027777777777777776}
In [12]:
nx.set_edge_attributes(G, values=distances, name='distance')
In [13]:
# view the nodes and optionally show their attribute data
G.nodes#(data=True)
Out[13]:
NodeView((0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, 62, 63, 64, 65, 66, 67, 68, 69, 70, 71, 72, 73, 74, 75, 76, 77, 78, 79, 80, 81, 82, 83, 84, 85, 86, 87, 88, 89, 90, 91, 92, 93, 94, 95, 96, 97, 98, 99))
In [14]:
# view the edges and optionally show their attribute data
# these are undirected edges, and there cannot be parallel edges
G.edges#(data=True)
Out[14]:
EdgeView([(0, 1), (0, 99), (0, 2), (0, 98), (1, 2), (1, 3), (1, 99), (2, 3), (2, 4), (2, 17), (3, 4), (3, 5), (4, 5), (4, 6), (4, 92), (5, 6), (5, 7), (6, 7), (6, 73), (7, 8), (7, 9), (8, 9), (8, 10), (9, 10), (9, 11), (9, 27), (10, 11), (10, 15), (11, 12), (11, 13), (12, 13), (12, 14), (12, 32), (13, 14), (13, 15), (14, 15), (14, 16), (15, 16), (15, 17), (16, 17), (16, 18), (17, 18), (18, 19), (18, 20), (19, 20), (19, 21), (20, 21), (20, 22), (20, 87), (21, 22), (21, 23), (22, 23), (22, 24), (23, 24), (23, 86), (24, 25), (24, 54), (24, 74), (25, 26), (25, 27), (26, 27), (26, 28), (27, 28), (28, 29), (28, 30), (29, 30), (29, 31), (30, 31), (30, 32), (31, 32), (31, 33), (32, 33), (33, 34), (33, 35), (34, 35), (34, 36), (34, 97), (35, 37), (35, 92), (35, 61), (36, 37), (36, 38), (37, 38), (37, 39), (38, 39), (38, 40), (39, 40), (39, 41), (40, 41), (40, 42), (40, 50), (41, 42), (41, 43), (42, 43), (42, 44), (43, 44), (43, 45), (44, 45), (44, 46), (45, 47), (45, 72), (46, 47), (46, 48), (47, 48), (47, 49), (48, 49), (48, 50), (49, 50), (49, 51), (49, 63), (50, 87), (51, 52), (51, 53), (52, 53), (52, 54), (53, 54), (53, 55), (54, 55), (54, 56), (55, 56), (55, 57), (56, 57), (56, 58), (57, 58), (57, 59), (58, 59), (58, 60), (59, 60), (59, 94), (60, 61), (60, 94), (61, 62), (62, 63), (62, 64), (63, 65), (64, 65), (64, 66), (65, 66), (65, 67), (66, 67), (66, 68), (67, 68), (67, 69), (68, 70), (68, 84), (69, 70), (69, 71), (70, 72), (70, 86), (71, 72), (71, 73), (72, 73), (72, 74), (73, 74), (73, 75), (74, 75), (75, 76), (75, 77), (76, 77), (76, 78), (77, 78), (77, 79), (78, 79), (78, 80), (78, 92), (79, 80), (79, 81), (80, 81), (80, 82), (81, 82), (81, 83), (82, 83), (82, 89), (83, 84), (83, 85), (84, 85), (84, 86), (85, 86), (85, 87), (86, 87), (86, 88), (87, 88), (88, 89), (88, 90), (89, 90), (89, 91), (90, 91), (90, 92), (91, 92), (91, 93), (93, 94), (93, 95), (94, 95), (94, 96), (95, 96), (95, 97), (96, 97), (96, 98), (97, 99), (98, 99)])
In [15]:
# calculate the shortest path between two nodes
path1 = nx.shortest_path(G, source=0, target=50)
path1
Out[15]:
[0, 2, 17, 18, 20, 87, 50]
In [16]:
# calculate the shortest weighted path between two nodes
path2 = nx.shortest_path(G, source=0, target=50, weight='distance')
path2
Out[16]:
[0, 2, 17, 18, 20, 87, 50]
In [17]:
# calculate node betweenness centrality across the network
bc = nx.betweenness_centrality(G, weight='distance')
pd.Series(bc).describe()
Out[17]:
count    100.000000
mean       0.048192
std        0.042308
min        0.000000
25%        0.014327
50%        0.039683
75%        0.069316
max        0.200577
dtype: float64

Q.1¶

In [ ]:
# now it's your turn
# try changing the social distance between our people, then recompute a shortest path

There is nothing explicitly spatial about the graph above. Although it models people and their relationships, it captures nothing about their positions in space. Now let's look at real-world spatial networks.

2. Spatial networks and OSMnx¶

OSMnx lets you download, model, analyze, and visualize street networks (and any other spatial data) anywhere in the world from OpenStreetMap.

OSMnx is built on top of GeoPandas, NetworkX, and matplotlib and interacts with OpenStreetMap’s APIs to:

  • Download and model street networks or other networked infrastructure anywhere in the world with a single line of code
  • Download any other spatial geometries, place boundaries, building footprints, or points of interest as a GeoDataFrame
  • Download by city name, polygon, bounding box, or point/address + network distance
  • Download drivable, walkable, bikeable, or all street networks
  • Download node elevations and calculate edge grades (inclines)
  • Impute missing speeds and calculate graph edge travel times
  • Simplify and correct the network’s topology to clean-up nodes and consolidate intersections
  • Fast map-matching of points, routes, or trajectories to nearest graph edges or nodes
  • Save networks to disk as shapefiles, GeoPackages, and GraphML
  • Save/load street network to/from a local .osm XML file
  • Conduct topological and spatial analyses to automatically calculate dozens of indicators
  • Calculate and visualize street bearings and orientations
  • Calculate and visualize shortest-path routes that minimize distance, travel time, elevation, etc
  • Visualize street networks as a static map or interactive Leaflet web map
  • Visualize travel distance and travel time with isoline and isochrone maps
  • Plot figure-ground diagrams of street networks and building footprints

More info:

  • OSMnx documentation
  • Examples, demos, tutorials
In [37]:
# download/model a street network for some city then visualize it
place = 'Ithaca, New York, USA'
G = ox.graph_from_place(place, network_type='rail')
fig, ax = ox.plot_graph(G)
---------------------------------------------------------------------------
ValueError                                Traceback (most recent call last)
Cell In [37], line 3
      1 # download/model a street network for some city then visualize it
      2 place = 'Ithaca, New York, USA'
----> 3 G = ox.graph_from_place(place, network_type='rail')
      4 fig, ax = ox.plot_graph(G)

File ~/anaconda3/envs/gds_py/lib/python3.9/site-packages/osmnx/graph.py:351, in graph_from_place(query, network_type, simplify, retain_all, truncate_by_edge, which_result, buffer_dist, clean_periphery, custom_filter)
    348 utils.log("Constructed place geometry polygon(s) to query API")
    350 # create graph using this polygon(s) geometry
--> 351 G = graph_from_polygon(
    352     polygon,
    353     network_type=network_type,
    354     simplify=simplify,
    355     retain_all=retain_all,
    356     truncate_by_edge=truncate_by_edge,
    357     clean_periphery=clean_periphery,
    358     custom_filter=custom_filter,
    359 )
    361 utils.log(f"graph_from_place returned graph with {len(G)} nodes and {len(G.edges)} edges")
    362 return G

File ~/anaconda3/envs/gds_py/lib/python3.9/site-packages/osmnx/graph.py:432, in graph_from_polygon(polygon, network_type, simplify, retain_all, truncate_by_edge, clean_periphery, custom_filter)
    429 poly_buff, _ = projection.project_geometry(poly_proj_buff, crs=crs_utm, to_latlong=True)
    431 # download the network data from OSM within buffered polygon
--> 432 response_jsons = downloader._osm_network_download(poly_buff, network_type, custom_filter)
    434 # create buffered graph from the downloaded data
    435 bidirectional = network_type in settings.bidirectional_network_types

File ~/anaconda3/envs/gds_py/lib/python3.9/site-packages/osmnx/downloader.py:534, in _osm_network_download(polygon, network_type, custom_filter)
    532     osm_filter = custom_filter
    533 else:
--> 534     osm_filter = _get_osm_filter(network_type)
    536 response_jsons = []
    538 # create overpass settings string

File ~/anaconda3/envs/gds_py/lib/python3.9/site-packages/osmnx/downloader.py:105, in _get_osm_filter(network_type)
    103     osm_filter = filters[network_type]
    104 else:  # pragma: no cover
--> 105     raise ValueError(f'Unrecognized network_type "{network_type}"')
    107 return osm_filter

ValueError: Unrecognized network_type "rail"

OSMnx geocodes the query "Ithaca, New York, USA" to retrieve the place boundaries of that city from the Nominatim API, retrieves the drivable street network data within those boundaries from the Overpass API, constructs a graph model, then simplifies/corrects its topology such that nodes represent intersections and dead-ends and edges represent the street segments linking them.

In [19]:
# look at the first 10 nodes: these are OSM IDs
list(G.nodes)[0:10]
Out[19]:
[213409446,
 213409480,
 213409892,
 213409894,
 213409896,
 213409899,
 213409959,
 213409964,
 213410337,
 213410339]
In [20]:
# look at the first 10 edges: u, v, key
list(G.edges)[0:10]
Out[20]:
[(213409446, 213409480, 0),
 (213409446, 213467056, 0),
 (213409446, 213467003, 0),
 (213409480, 213409446, 0),
 (213409480, 213449564, 0),
 (213409480, 213447279, 0),
 (213409892, 213472435, 0),
 (213409892, 213515843, 0),
 (213409892, 213409894, 0),
 (213409894, 213409896, 0)]
In [21]:
type(G)
Out[21]:
networkx.classes.multidigraph.MultiDiGraph

OSMnx models all networks as NetworkX MultiDiGraph objects. You can convert to:

  • undirected NetworkX MultiGraphs
  • NetworkX DiGraphs without (possible) parallel edges
  • GeoPandas node/edge GeoDataFrames
In [22]:
# convert your graph to node and edge GeoPandas GeoDataFrames
gdf_nodes, gdf_edges = ox.graph_to_gdfs(G)
gdf_nodes.head()
Out[22]:
y x street_count highway geometry
osmid
213409446 42.448787 -76.518574 3 NaN POINT (-76.51857 42.44879)
213409480 42.449783 -76.517811 3 NaN POINT (-76.51781 42.44978)
213409892 42.444591 -76.499413 3 NaN POINT (-76.49941 42.44459)
213409894 42.445959 -76.500512 3 NaN POINT (-76.50051 42.44596)
213409896 42.446884 -76.501260 4 stop POINT (-76.50126 42.44688)
In [23]:
gdf_edges.head()
Out[23]:
osmid name highway oneway reversed length geometry ref maxspeed lanes bridge access junction
u v key
213409446 213409480 0 20181025 Vinegar Hill residential False False 130.393 LINESTRING (-76.51857 42.44879, -76.51854 42.4... NaN NaN NaN NaN NaN NaN
213467056 0 345317460 Hector Street primary False False 504.350 LINESTRING (-76.51857 42.44879, -76.51851 42.4... NY 79 30 mph NaN NaN NaN NaN
213467003 0 345317460 Hector Street primary False True 292.613 LINESTRING (-76.51857 42.44879, -76.51873 42.4... NY 79 30 mph NaN NaN NaN NaN
213409480 213409446 0 20181025 Vinegar Hill residential False True 130.393 LINESTRING (-76.51781 42.44978, -76.51798 42.4... NaN NaN NaN NaN NaN NaN
213449564 0 20187426 Cliff Street primary False False 609.383 LINESTRING (-76.51781 42.44978, -76.51851 42.4... NY 96 NaN 2 NaN NaN NaN

You can create a graph from node/edge GeoDataFrames, as long as gdf_nodes is indexed by osmid and gdf_edges is multi-indexed by u, v, key (following normal MultiDiGraph structure). This allows you to load graph node/edge shapefiles or GeoPackage layers as GeoDataFrames then convert to a MultiDiGraph for graph analytics.

In [24]:
# convert node/edge GeoPandas GeoDataFrames to a NetworkX MultiDiGraph
G2 = ox.graph_from_gdfs(gdf_nodes, gdf_edges, graph_attrs=G.graph)
print(len(G2.nodes))
print(len(G2.edges))
621
1690

Q.2¶

In [ ]:
# now it's your turn
# download a graph of a different (small-ish) town, then plot it

Basic street network stats¶

In [25]:
# get our study site's geometry
gdf = ox.geocode_to_gdf(place)
gdf_proj = ox.project_gdf(gdf)
geom_proj = gdf_proj['geometry'].iloc[0]
geom_proj
Out[25]:
In [26]:
# what size area does our study site cover in square meters?
area_m = geom_proj.area
area_m
Out[26]:
15745858.508835593
In [27]:
# project the graph (automatically) then check its new CRS
G_proj = ox.project_graph(G)
G_proj.graph['crs']
Out[27]:
<Derived Projected CRS: +proj=utm +zone=18 +ellps=WGS84 +datum=WGS84 +unit ...>
Name: unknown
Axis Info [cartesian]:
- E[east]: Easting (metre)
- N[north]: Northing (metre)
Area of Use:
- undefined
Coordinate Operation:
- name: UTM zone 18N
- method: Transverse Mercator
Datum: World Geodetic System 1984
- Ellipsoid: WGS 84
- Prime Meridian: Greenwich
In [28]:
# show some basic stats about the (projected) network
ox.basic_stats(G_proj, area=area_m, clean_int_tol=10)
Out[28]:
{'n': 621,
 'm': 1690,
 'k_avg': 5.442834138486313,
 'edge_length_total': 227324.6080000004,
 'edge_length_avg': 134.51160236686414,
 'streets_per_node_avg': 3.0096618357487923,
 'streets_per_node_counts': {0: 0, 1: 82, 2: 2, 3: 368, 4: 167, 5: 1, 6: 1},
 'streets_per_node_proportions': {0: 0.0,
  1: 0.1320450885668277,
  2: 0.00322061191626409,
  3: 0.5925925925925926,
  4: 0.2689210950080515,
  5: 0.001610305958132045,
  6: 0.001610305958132045},
 'intersection_count': 539,
 'street_length_total': 122698.14400000003,
 'street_segment_count': 915,
 'street_length_avg': 134.0963322404372,
 'circuity_avg': 1.0565395120223608,
 'self_loop_proportion': 0.003278688524590164,
 'clean_intersection_count': 484,
 'node_density_km': 39.438941970139865,
 'intersection_density_km': 34.23122338471077,
 'edge_density_km': 14437.104707402268,
 'street_density_km': 7792.407376907997,
 'clean_intersection_density_km': 30.73824140667906}

More stats documentation

In [ ]:
### UPDATE THESE FILE PATHS!
# save graph to disk as geopackage (for GIS) or GraphML file (for Gephi etc)
ox.save_graph_geopackage(G, filepath='./data/mynetwork.gpkg')
ox.save_graphml(G, filepath='./data/mynetwork.graphml')

Visualize street centrality¶

Here we plot the street network and color its edges (streets) by their relative closeness centrality.

In [29]:
# convert graph to line graph so edges become nodes and vice versa
edge_centrality = nx.closeness_centrality(nx.line_graph(G))
nx.set_edge_attributes(G, edge_centrality, 'edge_centrality')
In [30]:
# color edges in original graph with centralities from line graph
ec = ox.plot.get_edge_colors_by_attr(G, 'edge_centrality', cmap='inferno')
fig, ax = ox.plot_graph(G, edge_color=ec, edge_linewidth=2, node_size=0)

Routing¶

In [31]:
# impute missing edge speeds then calculate edge (free-flow) travel times
G = ox.add_edge_speeds(G)
G = ox.add_edge_travel_times(G)
In [32]:
# get the nearest network nodes to two lat/lng points


orig = ox.nearest_nodes(G, -76.484735,42.450335)
dest = ox.nearest_nodes(G, -76.487419,42.440510)
In [33]:
# find the shortest path between these nodes, minimizing travel time, then plot it
route = ox.shortest_path(G, orig, dest, weight='travel_time')
fig, ax = ox.plot_graph_route(G, route, node_size=0)
In [34]:
# how long is our route in meters?
edge_lengths = ox.utils_graph.get_route_edge_attributes(G, route, 'length')
sum(edge_lengths)
Out[34]:
1529.3419999999996
In [35]:
# how far is it between these two nodes as the crow flies (haversine)?
ox.distance.great_circle_vec(G.nodes[orig]['y'], G.nodes[orig]['x'],
                             G.nodes[dest]['y'], G.nodes[dest]['x'])
Out[35]:
1180.4507452286598

Q.3¶

In [ ]:
# now it's your turn
# how circuitous is this route?
# try plotting it differently: change the colors and node/edge sizes

Get networks other ways¶

make queries less ambiguous to help the geocoder out, if it's not finding what you're looking for

In [ ]:
# you can make query an unambiguous dict to help the geocoder find it
place = {'city'   : 'San Francisco',
         'state'  : 'California',
         'country': 'USA'}
G = ox.graph_from_place(place, network_type='drive', truncate_by_edge=True)
fig, ax = ox.plot_graph(G, figsize=(10, 10), node_size=0, edge_color='y', edge_linewidth=0.2)
In [ ]:
# you can get networks anywhere in the world
G = ox.graph_from_place('Sinalunga, Italy', network_type='all')
fig, ax = ox.plot_graph(G, node_size=0, edge_linewidth=0.5)
In [ ]:
# or get network by address, coordinates, bounding box, or any custom polygon
# ...useful when OSM just doesn't already have a polygon for the place you want
sibley_hall = (42.450768,-76.484696)
one_mile = 1609 #meters
G = ox.graph_from_point(sibley_hall, dist=one_mile, network_type='drive')
fig, ax = ox.plot_graph(G, node_size=0)

Q.4¶

In [ ]:
# now it's your turn
# create a graph of your hometown then calculate the shortest path between two points of your choice

Get other networked infrastructure types¶

...like rail or electric grids or even the canals of Venice and Amsterdam, using the custom_filter parameter. See the Overpass Query Language documentation for query usage details.

In [ ]:
# get NY subway rail network
G = ox.graph_from_place('New York City, New York, USA',
                        retain_all=False, truncate_by_edge=True, simplify=True,
                        custom_filter='["railway"~"subway"]')

fig, ax = ox.plot_graph(G, node_size=0, edge_color='c', edge_linewidth=0.2)

Get any geospatial entities' geometries and attributes¶

Use the geometries module to download entities, such as local amenities, points of interest, or building footprints, and turn them into a GeoDataFrame.

In [38]:
# get all building footprints in some neighborhood
place = 'West Village, New York, New York, USA'
tags = {'building': True}
gdf = ox.geometries_from_place(place, tags)
gdf.shape
Out[38]:
(2259, 109)
In [50]:
gdf.explore(color='heritage')
Out[50]:
Make this Notebook Trusted to load map: File -> Trust Notebook
In [49]:
gdf['nycdoitt:bin']
Out[49]:
element_type  osmid    
node          368043320        NaN
              368043476        NaN
              368043569        NaN
              368043579        NaN
              368043604        NaN
                            ...   
relation      3357450      1010421
              3365138      1012047
              3365139      1012125
              3365140      1012154
              4462495      1077817
Name: nycdoitt:bin, Length: 2259, dtype: object
In [46]:
gdf.columns[:50]
Out[46]:
Index(['addr:state', 'building', 'ele', 'gnis:county_name', 'gnis:feature_id',
       'gnis:import_uuid', 'gnis:reviewed', 'name', 'source', 'geometry',
       'railway', 'wheelchair', 'addr:housenumber', 'addr:postcode',
       'addr:street', 'amenity', 'tourism', 'addr:city', 'access', 'nodes',
       'height', 'nycdoitt:bin', 'leisure', 'phone', 'website', 'atm', 'brand',
       'brand:wikidata', 'fax', 'opening_hours', 'short_name',
       'building:levels', 'roof:levels', 'start_date', 'building:material',
       'operator', 'operator:wikidata', 'operator:wikipedia', 'roof:shape',
       'wikidata', 'heritage', 'heritage:operator', 'historic',
       'nrhp:inscription_date', 'nrhp:nhl', 'protect_class',
       'protection_object', 'ref:nrhp', 'wikipedia', 'internet_access'],
      dtype='object')
In [ ]:
fig, ax = ox.plot_footprints(gdf, figsize=(3, 3))
In [ ]:
# get all parks and bus stops in some neighborhood
tags = {'leisure': 'park',
        'highway': 'bus_stop'}
gdf = ox.geometries_from_place(place, tags)
gdf.shape
In [ ]:
# restaurants near the empire state buildings
address = '350 5th Ave, New York, NY 10001'
tags = {'amenity': 'restaurant'}
gdf = ox.geometries_from_address(address, tags=tags, dist=500)
gdf[['name', 'cuisine', 'geometry']].dropna().head()

Q.5¶

In [ ]:
# now it's your turn
# find all the rail stations around downtown LA
# hint, the tag is railway and the value is station: https://wiki.openstreetmap.org/wiki/Tag:railway%3Dstation

If you're interested in more network analysis, check out:

  • The second notebook from this series
  • Other OSMnx case study notebooks that Geoff has created.